1. Estimate drag and momentum

·       In this section, I drive the car with a constant PWM for a long enough time to find its steady speed. As the lab7 instruction specified, I originally used active breaking of the car to avoid crashing into the wall: I set the PWM to 150 in the first part of car’s travel, and reuse the functionality from lab 5 to let the car stop in front of the wall to prevent it from breaking in the second part. I modified the code to achieve this combination:

if (curtime < startT + constpwmduration)

                {

                    u = 150;

                }

                else

                {

 

                    u = (p + i + d);

                    // deadband

                    if (0 < u && u < deadband)

                    {

                        u = deadband;

                    }

                    else if (-deadband < u && u < 0)

                    {

                        u = -deadband;

                    }

                    else if (u > 255) // motor's range

                    {

                        u = 255;

                    }

                    else if (u < -255)

                    {

                        u = -255;

                    }

                }

                setVelocity(u, u);

                appendLog(curtime - startT, -1, -1, curpos, -1, p, currealpos, d, dt, u);

where appendLog is function used to store the necessary values in Artemis, its parameters are

appendLog(int timeval, float LPFrollval, float LPFpitchval, float tfsval, float tfs2val, float Pval, float compudistval, float Dval, float dtval, int uval)

-1 is used to fill up the position of parameters that we don’t need in this lab.

·       I put the original u=p+i+d part into the else section in the if condition, where the constpwmduration is the duration of the constant PWM value in millisecond; when the condition is true, motor’s PWM is constant 150, when the time pass the set value, the if condition switch PWM to the stop in X meters from wall mode implemented in lab5.

·       The way to set constpwmduration is in the jupyter notebook: The command

 ble.send_command(CMD.RUN_TO_WALL_STOP,"1500|5000|45|0.78|135|0|50") #constpwmduration|run time|min nonzero speed|velocityfactor|kp|ki|kd

set the constpwmduration =1500 ms, total running time=5000ms.

·       However, then I realized that the Time of Flight sensor’s short distance sensoring mode restricts the reliable readings to be with 1.8m, if I persist to include the stop in X meters from wall mode, I wouldn’t have enough distance to confirm the steady speed, so I excluded it by setting constpwmduration to 4000—this time is long enough to ensured PWM is always in the constant value mode until it hit the wall. A paper box is used instead to protect my car. Although, the code above can be useful to achieve protection purposes if TOF sensor use long distance mode.

1.     The Bluetooth communication codes are the same as lab5. With a same procedure, I got the TOF readings, PWM values, and velocity against the time after driving the car toward the wall with PWM=150:

2.     Video of driving to wall:

3.     Plots:

         A graph with blue lines

Description automatically generated

               A graph with a line and numbers

Description automatically generated with medium confidence

              A graph with blue lines and orange lines

Description automatically generated

4.       I stored the data collected to  Lab7_data.csv using Pandas:

import pandas as pd

df = pd.DataFrame({

    'Time': timels,

    'TOF': tfls,

    'Motor inputs(PWM)': uls,

    'Velocity': vells

})

 

# Save to CSV

df.to_csv('Lab7_data.csv', index=False)

5.       and extract them back when needed:

df = pd.read_csv('Lab7_data.csv')

timels = df['Time'].to_numpy()

tfls = df['TOF'].to_numpy()

uls = df['Motor inputs(PWM)'].to_numpy()

vells = df['Velocity'].to_numpy()

 

6.       According to the data, the car hits the wall at t =2.165, so only the data before t=2.165 are meaningful for me.

A screenshot of a computer

Description automatically generated

The csv data shown above are Time, TOF, PWM, velocity respectively.

 

7.     By looking at the plots and averaging the last 3 unique meaning Velocity value in csv, I found that the steady speed =(-2.2843-2.3495-2.36998)/3=-2.35632 m/s, Tsteady=2.04 s; Thus 90% rise time = 2.04*0.9=1.836 s, and the speed at 90% risetime = -2.40.

1.0 
0.5 
0.0 
-1.0 
—1.5 
0.0 
0.5 
1.0 
Time 
1.5 
2.0 
2.5

 

 

2.  Initialize KF (Python)

8.     Computing d and m :

o   d=u/steady speed =1/ steady speed (since we assume u=1) = 1/2356.32 mm/s

o   m=-d*t0.9/ln(1-0.9)= -1/2356.32*1.836/ln(0.1)= 3.3839405 *10^4

9.     Matrix A and B:

o   Our state space equation is:

§  A black background with white text

Description automatically generated

So A= [ [0,1], [0,-d/m] ] = [ [0,1], [0,-d/m] ]= [[ 0,   1, ]  [ 0, -1.25413131 ] ], B= [[0], [1/m]] =[   [0], [2955.1346]], C=[-1,0]. They are all stored in array since NumPy provides lots of powerful functions.

10.  Process noise and sensor noise

o   Determine the delta_t between each TOF sensor readings

§  The car sends back all TOF sensor readings (the ones without interpolation) to python through Bluetooth, but that includes repetitive readings, so I implemented the codes below to remove the repetitive parts and use the list of unique TOF readings to construct an array of time stamps when these TOF readings are recorded; the array of PWM and TOF readings are also constructed along with the time array as they will also be used later; the array of u, tuz[1], is divided by 255 to convert its range to [0,1]:

# realposar: real position array

realposar = np.array(realposls)

# tuzls = the list of time, u(=control=PWM inputs), z(=observation=TOF readings) of each unique TOF sensor readings

# e.g. tuz[0] = a list of time of each unique TOF sensor readings

tuzls = [[], [], []]

prepos = 0

for i, pos in enumerate(realposar):

    if prepos != pos:

        tuzls[0].append(timels[i])

        tuzls[1].append(uls[i])

        tuzls[2].append(pos)

    prepos = pos

tuz = np.array(tuzls)

tuz[1] /= 255

 

§  Then call diff() to convert time array to delta_t array.

dtar_tof = np.diff(tuz[0])

 

o   Compute sigma_u and sigma_z

§  The average of my dtar_tof, the array of the dt between each TOF reading, was 0.11986 s; Substitute the average into the model below and got sigma_2=sigma_1= 28.884:

sigma_1=np.sqrt(10**2/np.average(dtar_tof))

sigma_2=sigma_1

sigma_3=20 # all length are in mm, but all time are in s

 

·       The position model used here is from lecture slides:

o   A black background with white text and yellow numbers

Description automatically generated

 

§  Plug sigma1,2,3 into the matrix:

sigma_u=np.array([[sigma_1**2,0],[0,sigma_2**2]])

sigma_z=np.array([[sigma_3**2]])

 

 

3. Implement and test your Kalman Filter in Jupyter (Python)

a.      Implement the KF() function

o   The function is defined below. it has two parts: prediction and updating. Since dt for each step may be different, I updated it by including dt in the parameters. In prediction part, I compute the Ad and Bd from A and B, then use Ad, Bd, previous state (mu & sigma) along with motor inputs and its stddev to predict the prediction state (mu_p & sigma_p); In updating stage, the prediction states is updated with the observation from TOF sensor. The kkf_gain shows how much I trust TOF’s readings: the higher the kkf_gain is, the more I trust TOF readings.

def kf(mu,sigma,u,z, dt):#mu and sigma are the prior state; u is control, z is observation(sensor reading)

    #prediction: produce a prediction based on the prior state&control

    Ad=np.eye(2) + dt*A

    Bd=dt*B

    mu_p = Ad.dot(mu) + Bd.dot(u)    

    sigma_p = Ad.dot(sigma.dot(Ad.transpose())) + sigma_u

 

    #update: use obvservation to update the prediction

    sigma_m = C.dot(sigma_p.dot(C.transpose())) + sigma_z

    kkf_gain = sigma_p.dot(C.transpose().dot(np.linalg.inv(sigma_m)))

    mu = mu_p + kkf_gain.dot(z-C.dot(mu_p))         # a formula that denoises mu

    sigma=(np.eye(2)-kkf_gain.dot(C)).dot(sigma_p)  # a formula that denoises sigma

 

    return mu,sigma, mu_p

b.     Choose the initial state of KF readings

o    The Kalman Filter need previous state to work, so the initial state must be given before kf() is called, it is defined as follows, where mu_kf starts with TOF’s 1st reading and the initial velocity, and var_kf starts with empirical value ([[20, 20], [20, 20]]

# final len(kf) should==num+1

# kf=[mu list, sigma list]

mu_kf = [np.array([[tuz[2][0]],[vells[0]]]),]

var_kf = [np.array([[20, 20], [20, 20]]),]

 

c.      Loop through all steps and get KF result

o   Last, call KF after each step to predict the next one:

_, num = np.shape(tuz)

#num-1 since dt is 1 less than length of time,u,z

mu_p_ls=[np.array([[tuz[2][0]],[vells[0]]]),]

for i in range(num-1):

    mu, sigma,mu_p = kf(mu_kf[i], var_kf[i], tuz[1][i], -tuz[2][i],dtar_tof[i])

    mu_kf.append(mu)

    var_kf.append(sigma)

    mu_p_ls.append(mu_p)

 

 

o   Plot the result:

§  A graph of a function

Description automatically generated

§  As the plot show, the prediction is made and then updated successfully; the KF result is very close to the TOF readings,

§  The U’s plot is consistent with the curves above:

§  A graph with blue dots

Description automatically generated